﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.IO;

using Rockfan.Framework.Audio.Midi;

namespace Rockfan.Framework.Audio
{
    public sealed class Chart
    {
        public BeatTrack Beats { get; private set; }
        public TempoTrack TempoTrack { get; private set; }
        public TimeSignatureTrack TimeSignatureTrack { get; private set; }

        public NoteTracks GuitarTracks { get; private set; }
        public NoteTracks AlternateGuitarTracks { get; private set; }
        public NoteTracks DrumTracks { get; private set; }
        public NoteTracks VocalTracks { get; private set; }
        public NoteTracks KeyboardTracks { get; private set; }
        public NoteTracks ProGuitarTracks { get; private set; }
        public NoteTracks ProAlternateGuitarTracks { get; private set; }
        public NoteTracks ProDrumTracks { get; private set; }

        private Chart()
        {
            Beats = new BeatTrack("Beat");
            TempoTrack = new TempoTrack("Tempo");
            TimeSignatureTrack = new TimeSignatureTrack("TimeSignature");

            GuitarTracks = new NoteTracks("Guitar");
            AlternateGuitarTracks = new NoteTracks("AlternateGuitar");
            DrumTracks = new NoteTracks("Drums");
            VocalTracks = new NoteTracks("Vocals");
            KeyboardTracks = new NoteTracks("Keybaord");
            ProGuitarTracks = new NoteTracks("ProGuitar");
            ProAlternateGuitarTracks = new NoteTracks("ProAlternate");
            ProDrumTracks = new NoteTracks("ProDrums");
        }

        public static Chart Load(string directoryLocation)
        {
            var cv3File = Directory.GetFiles(directoryLocation, "notes.cv3", SearchOption.TopDirectoryOnly);
            var midiFile = Directory.GetFiles(directoryLocation, "notes.mid", SearchOption.TopDirectoryOnly);
            var chartFile = Directory.GetFiles(directoryLocation, "notes.chart", SearchOption.TopDirectoryOnly);

            var chart = new Chart();

            if (cv3File.Length > 0)
                return ParseFromCV3(cv3File[0]);
            else if (midiFile.Length > 0)
                return ParseFromMidi(midiFile[0]);

            else if (chartFile.Length > 0)
                return ParseFromChart(chartFile[0]);

            throw new FileNotFoundException();

            //var fileLocation = directoryLocation + @"\notes.mid";
            //var parser = Parser.Load(fileLocation);

            //chart.Beats = parser.Beats;
            //chart.TempoTrack = parser.TempoTrack;
            //chart.TimeSignatureTrack = parser.TimeSignatureTrack;

            //foreach (var track in parser.NoteTracks)
            //{
            //    var noteTrack = track;
            //    var tracks = default(NoteTracks);

            //    LoadNoteTrack(ref noteTrack, out tracks);

            //    if (track.Name == "PART GUITAR")
            //    {
            //        chart.GuitarTracks = tracks;
            //        chart.GuitarTracks.Beats = chart.Beats;
            //        chart.GuitarTracks.TimeSignatureTrack = chart.TimeSignatureTrack;
            //    }
            //    else if (track.Name == "PART BASS")
            //    {
            //        chart.AlternateGuitarTracks = tracks;
            //        chart.AlternateGuitarTracks.Beats = chart.Beats;
            //        chart.AlternateGuitarTracks.TimeSignatureTrack = chart.TimeSignatureTrack;
            //    }
            //}

            //return chart;
        }

        private static Chart ParseFromCV3(string fileLocation)
        {

            return default(Chart);
        }

        private static Chart ParseFromMidi(string fileLocation)
        {
            var chart = new Chart();
            var midi = MidiFormat.Load(fileLocation);

            var beats = 0;
            var songEnd = 0;
            var songStart = 0;
            var tempo = midi.PPQN * 1000.0f;
            var bpm = 6E+07f / (float)tempo;
            
            var eightNote = (int)(midi.PPQN / 2.0f);

            foreach (var tempoEvent in midi.TempoTrack.Events)
            {
                var tEvent = (TempoEvent)tempoEvent;
                var time = midi.GetSeconds(tEvent.Start);
                chart.TempoTrack.AddEvent(new TempoTrackEvent(time, tEvent.BPM));
            }

            foreach (var timeSigEvent in midi.TimeSignatureTrack.Events)
            {
                var tsEvent = (TimeSignatureEvent)timeSigEvent;
                var time = midi.GetSeconds(tsEvent.Start);
                chart.TimeSignatureTrack.AddEvent(new TimeSignatureTrackEvent(time, tsEvent.Numerator, tsEvent.Denominator));
            }

            #region Get Note Tracks

            for (int i = 0; i < midi.Count; i++)
            {
                var midiTrack = midi[i];
                var sections = new List<Section>();
                var eventList = new List<NoteEvent>();
                var trackName = midiTrack.Name;

                foreach (var trackEvent in midiTrack.Events)
                {
                    if (trackEvent is NoteEvent)
                    {
                        var noteEvent = (NoteEvent)trackEvent;
                        var noteID = noteEvent.NoteID;

                        #region Get Notes

                        if (!noteEvent.IsNoteOn && noteID >= 60)
                        {
                            for (int index = eventList.Count - 1; index >= 0; index--)
                            {
                                var startEvent = eventList[index];

                                if (startEvent.NoteID == noteEvent.NoteID)
                                {
                                    var noteTracks = default(NoteTracks);
                                    var startPosition = midi.GetSeconds(startEvent.Start);
                                    var endPosition = midi.GetSeconds(noteEvent.Start);

                                    var length = Math.Abs(startPosition - endPosition);                                   
                                    var noteType = (NoteType)Math.Pow(2.0f, (int)((noteID - 60) % 12) + 1);
                                    var startTime = startEvent.Start <= noteEvent.Start ? startPosition : endPosition;

                                    if (length < midi.PPQN / 2.0f)
                                        length = 0;                                    

                                    if (trackName.ToLower() == "part guitar")
                                        noteTracks = chart.GuitarTracks;

                                    else if (trackName.ToLower() == "part bass")
                                        noteTracks = chart.AlternateGuitarTracks;

                                    else if (trackName.ToLower() == "part drums")
                                        noteTracks = chart.DrumTracks;

                                    #region Set Appropriate Difficulty

                                    switch (noteID)
                                    {
                                        #region Easy Notes

                                        case 60:
                                        case 61:
                                        case 62:
                                        case 63:
                                        case 64:
                                            noteTracks.Easy.AddEvent(new NoteTrackEvent(startTime, length, noteType, Difficulty.Easy));
                                            break;

                                        case 67:
                                            noteTracks.Easy.PowerNoteSections.Add(new Section("PowerSection", startTime, length));
                                            break;

                                        case 69:
                                            noteTracks.Easy.Player1NoteSection.Add(new Section("Player1Section", startTime, length));
                                            break;

                                        case 70:
                                            noteTracks.Easy.Player2NoteSection.Add(new Section("Player2Section", startTime, length));
                                            break;

                                        #endregion

                                        #region Medium Notes

                                        case 72:
                                        case 73:
                                        case 74:
                                        case 75:
                                        case 76:
                                            noteTracks.Medium.AddEvent(new NoteTrackEvent(startTime, length, noteType, Difficulty.Easy));
                                            break;

                                        case 79:
                                            noteTracks.Medium.PowerNoteSections.Add(new Section("PowerSection", startTime, length));
                                            break;

                                        case 81:
                                            noteTracks.Medium.Player1NoteSection.Add(new Section("Player1Section", startTime, length));
                                            break;

                                        case 82:
                                            noteTracks.Medium.Player2NoteSection.Add(new Section("Player2Section", startTime, length));
                                            break;

                                        #endregion

                                        #region Hard Notes

                                        case 84:
                                        case 85:
                                        case 86:
                                        case 87:
                                        case 88:
                                            noteTracks.Hard.AddEvent(new NoteTrackEvent(startTime, length, noteType, Difficulty.Easy));
                                            break;

                                        case 91:
                                            noteTracks.Hard.PowerNoteSections.Add(new Section("PowerSection", startTime, length));
                                            break;

                                        case 93:
                                            noteTracks.Hard.Player1NoteSection.Add(new Section("Player1Section", startTime, length));
                                            break;

                                        case 94:
                                            noteTracks.Hard.Player2NoteSection.Add(new Section("Player2Section", startTime, length));
                                            break;

                                        #endregion

                                        #region Expert Notes

                                        case 96:
                                        case 97:
                                        case 98:
                                        case 99:
                                        case 100:
                                            noteTracks.Expert.AddEvent(new NoteTrackEvent(startTime, length, noteType, Difficulty.Easy));
                                            break;

                                        case 103:
                                            noteTracks.Expert.PowerNoteSections.Add(new Section("PowerSection", startTime, length));
                                            break;

                                        case 105:
                                            noteTracks.Expert.Player1NoteSection.Add(new Section("Player1Section", startTime, length));
                                            break;

                                        case 106:
                                            noteTracks.Expert.Player2NoteSection.Add(new Section("Player2Section", startTime, length));
                                            break;

                                        #endregion
                                    }

                                    #endregion

                                    eventList.Remove(startEvent);
                                }
                            }
                        }
                        else if (noteID >= 60)
                            eventList.Add(noteEvent);

                        #endregion
                    }
                    else if (trackEvent is TextEvent)
                    {
                        var textEvent = (TextEvent)trackEvent;

                        if (textEvent.Text.ToLower() == "[music_start]")
                            songStart = textEvent.Start;

                        else if (textEvent.Text.ToLower() == "[end]")
                            songEnd = textEvent.Start;
                    }
                }
            }

            #endregion

            #region Generate Hopo

            GenerateHopo(midi, chart.GuitarTracks, (int)((midi.PPQN * 4.0f) / 12.0f));
            GenerateHopo(midi, chart.AlternateGuitarTracks, (int)((midi.PPQN * 4.0f) / 12.0f));

            #endregion
            
            #region Construct Beat Track

            var numerator = 4;
            var denominator = 4;
            var runningStatus = songStart;

            while (runningStatus <= songEnd)
            {
                var id = MarkerType.Measure;

                foreach (var tEvent in midi.TempoTrack.Events)
                {
                    if (tEvent.Start + songStart <= runningStatus)
                        bpm = (tEvent as TempoEvent).BPM;

                    else if (tEvent.Start + songStart > runningStatus)
                        break;
                }

                foreach (var tsEvent in midi.TimeSignatureTrack.Events)
                {
                    if (tsEvent.Start + songStart <= runningStatus)
                    {
                        numerator = (tsEvent as TimeSignatureEvent).Numerator;
                        denominator = (tsEvent as TimeSignatureEvent).Denominator;
                    }
                    else if (tsEvent.Start + songStart > runningStatus)
                        break;
                }

                // Set the marker event as an Measure Event
                if (beats % (numerator * denominator) == 0)
                    id = MarkerType.Measure;

                // Set the marker event as a Beat event
                else if ((beats * numerator) % (numerator * denominator) == 0)
                    id = MarkerType.Beat;

                // Set the marker event as a eigth note event
                else id = MarkerType.Eigth;

                chart.Beats.AddEvent(new BeatTrackEvent(midi.GetSeconds(runningStatus), id, bpm));
                runningStatus += eightNote;
                beats++;
            }

            #endregion

            #region Sort all Tracks

            chart.Beats.SortAccending();
            chart.AlternateGuitarTracks.SortAccending();
            chart.DrumTracks.SortAccending();
            chart.GuitarTracks.SortAccending();
            chart.KeyboardTracks.SortAccending();
            chart.ProAlternateGuitarTracks.SortAccending();
            chart.ProDrumTracks.SortAccending();
            chart.ProGuitarTracks.SortAccending();
            chart.TempoTrack.SortAccending();
            chart.TimeSignatureTrack.SortAccending();
            chart.VocalTracks.SortAccending();

            #endregion

            return chart;
        }

        private static void GenerateHopo(MidiFormat midi, NoteTracks tracks, int hopoValue)
        {
            GenerateHopo(midi, tracks.Easy, hopoValue);
            GenerateHopo(midi, tracks.Medium, hopoValue);
            GenerateHopo(midi, tracks.Hard, hopoValue);
            GenerateHopo(midi, tracks.Expert, hopoValue);
            GenerateHopo(midi, tracks.ExpertPlus, hopoValue);
        }

        private static void GenerateHopo(MidiFormat midi, NoteTrack track, int hopoValue)
        {
            int lastNoteLenghtOffset = 0;
            float lastNoteCount = 0;
            float lastNoteTime = 0.0f;
            var noteType = NoteType.Dead;

            for (int i = 0; i < track.Count; i++)
            {
                if (i == 0)
                {
                    var startOffset = midi.GetOffset(track[i].Start);
                    var endOffset = midi.GetOffset(track[i].Start + track[i].Length);
                    lastNoteLenghtOffset = endOffset - startOffset;
                    lastNoteTime = track[i].Start + track[i].Length;
                    noteType = track[i].ID;
                    continue;
                }

                var note = track[i];

                if (lastNoteTime != note.Start + note.Length)
                {
                    var startOffset = midi.GetOffset(note.Start);
                    var endOffset = midi.GetOffset(note.Start + note.Length);
                    var lastOffset = midi.GetOffset(lastNoteTime);
                    var currentOffset = midi.GetOffset(note.Start + note.Length);

                    if (lastOffset + hopoValue >= currentOffset && lastNoteLenghtOffset <= midi.PPQN)
                    {
                        var noteCount = GetNoteCount(track, note.Start);

                        if (lastNoteCount == 1 && noteCount == 1)
                        {
                            if (!noteType.HasFlag(note.ID))
                            {
                                note.ID = note.ID | NoteType.Hopo;
                                noteType = note.ID;                                
                                lastNoteTime = note.Start + note.Length;
                                lastNoteCount = noteCount;
                                lastNoteLenghtOffset = endOffset - startOffset;
                                continue;
                            }
                        }
                        else if (lastNoteCount >= 2 && noteCount == 1)
                        {
                            if (!noteType.HasFlag(note.ID))
                            {
                                note.ID = note.ID | NoteType.Hopo;
                                noteType = note.ID;
                                lastNoteTime = note.Start + note.Length;
                                lastNoteCount = noteCount;
                                lastNoteLenghtOffset = endOffset - startOffset;
                                continue;
                            }
                        }
                        else if (lastNoteCount == 1 && noteCount >= 2)
                        {
                            if (!noteType.HasFlag(note.ID))
                            {
                                note.ID = note.ID | NoteType.Hopo;
                                noteType = note.ID;
                                lastNoteTime = note.Start + note.Length;
                                lastNoteCount = noteCount;
                                lastNoteLenghtOffset = endOffset - startOffset;
                                continue;
                            }
                        }
                        else
                        {
                            lastNoteTime = note.Start + note.Length;
                            lastNoteCount = noteCount;
                            lastNoteLenghtOffset = endOffset - startOffset;
                            continue;
                        }
                    }
                    else
                    {
                        lastNoteCount = GetNoteCount(track, note.Start);
                        lastNoteTime = note.Start + note.Length;
                        noteType = note.ID;
                        lastNoteLenghtOffset = endOffset - startOffset;
                        continue;
                    }
                }

                if (lastNoteTime == note.Start && noteType.HasFlag(NoteType.Hopo))
                {   
                    noteType |= note.ID;
                    note.ID |= NoteType.Hopo;
                }
            }
        }

        private static void ResetHopo(NoteTrack track, float delta)
        {
            for (int i = 0; i < track.Count; i++)
            {
                if (track[i].Start < delta)
                    continue;

                if (track[i].Start == delta)
                    track[i].ID &= ~NoteType.Hopo;

                else if (track[i].Start > delta)
                    break;
            }
        }

        private static int GetNoteCount(NoteTrack track, float delta)
        {
            var noteCount = 0;

            for (int i = 0; i < track.Count; i++)
            {
                if (track[i].Start < delta)
                    continue;

                if (track[i].Start == delta)
                    noteCount++;

                else if (track[i].Start > delta)
                    break;
            }

            return noteCount;
        }

        private static Chart ParseFromChart(string fileLocation)
        {

            return default(Chart);
        }

        private void WriteToFile(string directory)
        {

        }

        private static void LoadNoteTrack(ref NoteTrack track, out NoteTracks tracks)
        {
            tracks = new NoteTracks(track.Name);
            
            for (int i = 0; i < track.Count; i++)
            {
                if (track[i] is NoteTrackEvent)
                {
                    var note = (NoteTrackEvent)track[i];

                    if (note.Difficulty == Difficulty.Easy)
                        tracks.Easy.AddEvent(note);

                    else if (note.Difficulty == Difficulty.Medium)
                        tracks.Medium.AddEvent(note);

                    else if (note.Difficulty == Difficulty.Hard)
                        tracks.Hard.AddEvent(note);

                    else if (note.Difficulty == Difficulty.Expert)
                        tracks.Expert.AddEvent(note);
                }
            }
        }
    }

}
